package boids.preys;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;

import boids.*;
import boids.predators.Predator;

import util.Vector;

public class ExponentialPrey extends AbstractPrey
{
	private double sepFromClosestNeightborCurveWidth;
	private double separationCurveWidth;
	private double cohesionCurveWidth;
	private double alignmentCurveWidth;
	private double envConstraintCurveWidth;
	private double escapeFromClosestPredatorCurveWidth;
	private double escapeCurveWidth;
	
	public ExponentialPrey(Vector location, Vector velocity, double width, double height)
	{
		super(location, velocity, width, height);
		
		sepFromClosestNeightborCurveWidth = curveWidth(SEPARATION_RADIUS, 1, 0.05);
		separationCurveWidth = curveWidth(SEPARATION_RADIUS, 1, 0.05);
		cohesionCurveWidth = curveWidth(0, PERCEPTION_RADIUS, 0.05);
		alignmentCurveWidth = curveWidth(0, PERCEPTION_RADIUS, 0.05);
		envConstraintCurveWidth = curveWidth(PERCEPTION_RADIUS, 1, 0.005);
		escapeFromClosestPredatorCurveWidth = curveWidth(ESCAPE_RADIUS, 1, 0.05);
		escapeCurveWidth = curveWidth(ESCAPE_RADIUS, 1, 0.05);
	}
	
	private double curveWidth(double minValueXShift, double maxValueXShift, double minValue)
	{
		return -1 * (Math.pow(minValueXShift - maxValueXShift, 2) / Math.log(minValue));
	}
	
	public void update(ArrayList<Prey> preys, ArrayList<Predator> predators)
	{
		// preys are lazy and don't like to on the move
		slowDown();
		
		Vector acceleration = new Vector();
		
		// change in movement to stay within environment boundaries
		acceleration.add(environMentConstraint());
		
		// change in movement according to flock/swarm behavior
		ArrayList<Prey> neighbors = findNeighbors(preys);
		if (neighbors.size() > 0)
		{		
			acceleration.add(separationFromClosestBoid(neighbors, sepFromClosestNeightborCurveWidth, SEPARATION_RADIUS));
			acceleration.add(separationFromAverage(neighbors, separationCurveWidth, SEPARATION_RADIUS));
			acceleration.add(cohesion(neighbors));
			acceleration.add(alignment(neighbors));		
		}
		
		// TODO: change in movement to avoid predator
		// ideas: add escape vector x time steps longer than alarmed to gain distance to pred
		ArrayList<Predator> predatorsInRange = findPredators(predators);
		Vector escapeAcceleration = new Vector();
		if (alarmed)
		{
			MIN_FORCE = 0;
			escapeAcceleration.add(separationFromClosestBoid(predatorsInRange, escapeFromClosestPredatorCurveWidth, PERCEPTION_RADIUS));
			escapeAcceleration.add(separationFromAverage(predatorsInRange, escapeCurveWidth, PERCEPTION_RADIUS));
		}
		else
		{
			if (MIN_FORCE <= MAX_FORCE) MIN_FORCE += MIN_FORCE * 0.05;
			if (MIN_FORCE == 0) MIN_FORCE += 0.1;
		}
		
		// add total acceleration to velocity. total acceleration = (general movement + escape) / 2
		acceleration.limit(MAX_FORCE);
		acceleration.add(escapeAcceleration);
		acceleration.limit(MAX_FORCE);
		velocity.add(acceleration);
		
		// keep velocity within boundaries
		velocity.limit(MAX_SPEED);
		velocity.cutOff(MAX_SPEED * 0.10);
		
		location.add(velocity);
	}
	
	private void slowDown()
	{
		Vector negVelocity = velocity.clone().multiply(-1);
		double width = curveWidth(0, MAX_SPEED, 0.5);
		double factor = Math.exp(- (1.0 / width) * Math.pow((negVelocity.magnitude() - MAX_SPEED), 2));
		negVelocity.multiply(factor);
		
		velocity.add(negVelocity);
	}
	
	private Vector environMentConstraint()
	{
		// vector pointing away from the edge whose component's magnitude
		// represents the distance respectively
		Vector distanceToEdge = distanceToEdge();
		Vector xForce = new Vector(distanceToEdge.getX(), 0);
		Vector yForce = new Vector(0, distanceToEdge.getY());
		
		double xFactor = Math.exp(- (1.0 / envConstraintCurveWidth) * Math.pow(xForce.magnitude() - 1, 2));
		double yFactor = Math.exp(- (1.0 / envConstraintCurveWidth) * Math.pow(yForce.magnitude() - 1, 2));

		xForce.limit(MAX_FORCE);
		yForce.limit(MAX_FORCE);
		
		xForce.multiply(xFactor);
		yForce.multiply(yFactor);
		
		return xForce.add(yForce);
	}
	
	private Vector distanceToEdge()
	{
		Vector distanceToEdge = new Vector();
		
		if (location.getX() <= PERCEPTION_RADIUS)
		{
			distanceToEdge.add(new Vector(location.getX(), 0));
		}
		else if (envWidth - location.getX() <= PERCEPTION_RADIUS)
		{
			distanceToEdge.add(new Vector(location.getX() - envWidth, 0));
		}
		
		if (location.getY() <= PERCEPTION_RADIUS)
		{
			distanceToEdge.add(new Vector(0, location.getY()));
		}
		else if (envHeight - location.getY() <= PERCEPTION_RADIUS)
		{
			distanceToEdge.add(new Vector(0, location.getY() - envHeight));
		}
		
		return distanceToEdge;
	}
	
	private Vector escape(ArrayList<? extends Boid> boids, double curveWidth)
	{
		Vector closestNeighbor = new Vector();
		double smallestDistance = Double.MAX_VALUE;
		
		for (Boid b : boids)
		{
			double distance = location.euclDistance(b.getLocation());
			
			if (distance <= SEPARATION_RADIUS && distance < smallestDistance)
			{
				smallestDistance = distance;
				closestNeighbor = b.getLocation();
			}
		}
		
		if (smallestDistance > SEPARATION_RADIUS)
		{
			return new Vector();
		}
		
		Vector force = Vector.substract(location, closestNeighbor);
		double factor = Math.exp(- (1.0 / curveWidth) * Math.pow((smallestDistance - 1), 2));
		force.limit(MAX_FORCE);
		force.multiply(factor);
		
		return force.cutOff(MIN_FORCE);
	}
	
	private Vector separationFromClosestBoid(ArrayList<? extends Boid> boids, double curveWidth, double radius)
	{
		Vector closestNeighbor = new Vector();
		double smallestDistance = Double.MAX_VALUE;
		
		for (Boid b : boids)
		{
			double distance = location.euclDistance(b.getLocation());
			
			if (distance <= radius && distance < smallestDistance)
			{
				smallestDistance = distance;
				closestNeighbor = b.getLocation();
			}
		}
		
		if (smallestDistance > radius)
		{
			return new Vector();
		}
		
		Vector force = Vector.substract(location, closestNeighbor);
		double factor = Math.exp(- (1.0 / curveWidth) * Math.pow((smallestDistance - 1), 2));
		force.limit(MAX_FORCE);
		force.multiply(factor);
		
		return force.cutOff(MIN_FORCE);
	}
	
	private Vector separationFromAverage(ArrayList<? extends Boid> boids, double curveWidth, double radius)
	{
		Vector separationMean = new Vector();
		int counter = 0;
		
		for (Boid b : boids)
		{
			double distance = location.euclDistance(b.getLocation());
			
			if (distance <= radius)
			{
				separationMean.add(b.getLocation());
				counter++;
			}
		}
		
		if (counter == 0)
		{
			return new Vector();
		}
		
		separationMean.divide(counter);
		Vector force = Vector.substract(location, separationMean);
		double factor = Math.exp(- (1.0 / curveWidth) * Math.pow((force.magnitude() - 1), 2));
		force.limit(MAX_FORCE);
		force.multiply(factor);
		
		return force.cutOff(MIN_FORCE);
	}
	
	private Vector cohesion(ArrayList<? extends Boid> neighbors)
	{
		Vector cohesionMean = new Vector();
		
		for (Boid b : neighbors)
		{
			cohesionMean.add(b.getLocation());
		}
		
		cohesionMean.divide(neighbors.size());
		Vector force = Vector.substract(cohesionMean, location);
		double factor = Math.exp(-1 * (1.0 / cohesionCurveWidth) * Math.pow((force.magnitude() - PERCEPTION_RADIUS), 2));
		force.limit(MAX_FORCE);
		force.multiply(factor);
		
		return force.cutOff(MIN_FORCE);
	}
	
	private Vector alignment(ArrayList<? extends Boid> neighbors)
	{
		Vector alignmentMean = new Vector();
		
		for (Boid b : neighbors)
		{
			alignmentMean.add(b.getVelocity());
		}
		
		alignmentMean.divide(neighbors.size());
		double velDifference = Math.abs(Vector.substract(oldVelocity, alignmentMean).magnitude());
		double factor = Math.exp(-1 * (1.0 / alignmentCurveWidth) * Math.pow((velDifference - 2 * MAX_SPEED), 2));
		
		alignmentMean.multiply(factor);
		
		return alignmentMean.cutOff(MIN_FORCE);
	}
}
